【初心者向け】S3バケット内の特定ファイルを一括で別フォルダに移動するコマンド

【初心者向け】S3バケット内の特定ファイルを一括で別フォルダに移動するコマンド

Clock Icon2024.08.29

はじめに

データ事業本部ビッグデータチームのyosh-kです。
今回はaws cliコマンドを用いて特定の条件においてのファイル移動を実現したいと思います。

前提

現状のあるS3 Bucket上のフォルダ構成になります。test_a.txt,test_b.txt, test_c.txtを一つのコマンドでdoneに移動したいと思います。コマンドラインに慣れている人にとっては簡単だと思いますが、私は慣れていないので記録用にも残しておきます。

@ tmp % aws s3 ls s3://cm-kasama-cli-test/ --recursive --profile <my-profile>
2024-08-27 21:27:11          0 test-1/test-a/done/
2024-08-27 21:35:02          2 test-1/test-a/test_a.txt
2024-08-27 21:27:12          0 test-1/test-b/done/
2024-08-27 21:35:03          2 test-1/test-b/test_b.txt
2024-08-27 21:27:13          0 test-1/test-c/done/
2024-08-27 21:35:03          2 test-1/test-c/test_c.txt
2024-08-27 21:32:06          9 test-1/test_a_header.txt
2024-08-27 21:32:08          9 test-1/test_b_header.txt
2024-08-27 21:32:09          9 test-1/test_c_header.txt
tmp % 

上記を実現する上で今回使用するのは、aws s3api, |(パイプライン), xargsです。

aws s3api

aws s3apiコマンドは、aws s3コマンドでカバーしていない S3 の API 操作についても実行可能なコマンドです。その中でも今回はlist-objects-v2コマンドを使用します。

https://dev.classmethod.jp/articles/cli-command-s3-s3-and-s3api-i-tried-to-find-out-what-i-can-do/
https://docs.aws.amazon.com/cli/latest/reference/s3api/list-objects-v2.html

list-objects-v2は、オブジェクトのlistを取得するコマンドで、その中でもqueryオプションを使用します。queryオプションは、JMESPath (JSON Matching Expression Path) というクエリ言語を使用して、JSON 形式の出力結果をフィルタリングし、必要な情報だけを抽出することができます。

https://tech.itandi.co.jp/entry/2019/02/12/150000
https://jmespath.org/tutorial.html

| (パイプライン)

Linuxの|(パイプライン)は、あるコマンドの出力を別のコマンドの入力として直接渡すための機能です。これにより、複数のコマンドを連結して、より複雑な操作を行うことができます。

https://www.miraiserver.ne.jp/column/about_pipeline_command/

例えば、ls -lの出力結果にパイプラインでsortコマンドを実行することで出力を昇順にできます。

@ blog_tmp % ls -l
total 48
-rw-r--r--@ 1   staff  2 Aug 27 20:12 test_a.txt
-rw-r--r--@ 1   staff  9 Aug 27 20:12 test_a_header.txt
-rw-r--r--@ 1   staff  2 Aug 27 20:46 test_b.txt
-rw-r--r--@ 1   staff  9 Aug 27 20:13 test_b_header.txt
-rw-r--r--@ 1   staff  2 Aug 27 20:46 test_c.txt
-rw-r--r--@ 1   staff  9 Aug 27 20:46 test_c_header.txt
@ blog_tmp % ls -l | sort
-rw-r--r--@ 1   staff  2 Aug 27 20:12 test_a.txt
-rw-r--r--@ 1   staff  2 Aug 27 20:46 test_b.txt
-rw-r--r--@ 1   staff  2 Aug 27 20:46 test_c.txt
-rw-r--r--@ 1   staff  9 Aug 27 20:12 test_a_header.txt
-rw-r--r--@ 1   staff  9 Aug 27 20:13 test_b_header.txt
-rw-r--r--@ 1   staff  9 Aug 27 20:46 test_c_header.txt
total 48
@ blog_tmp % 

xargs

xargsコマンドは、標準入力からデータを受け取り、そのデータを別のコマンドの引数として使用することができます。

https://atmarkit.itmedia.co.jp/ait/articles/1801/19/news014.html
https://yossi-note.com/about_xargs/

以下は具体的なコマンドの使用例になります。

# 1. 基本的なxargsの使用
@ blog_tmp % echo "apple banana cherry" | xargs echo "Fruits:"
Fruits: apple banana cherry

# 2. -n1 オプションを使用
@ blog_tmp % echo "apple banana cherry" | xargs -n1 echo "Fruit:"
Fruit: apple
Fruit: banana
Fruit: cherry

# 3. -n1 と -I {} を使用
@ blog_tmp % echo "apple banana cherry" | xargs -n1 -I {} echo "The fruit is: {}"
The fruit is: apple
The fruit is: banana
The fruit is: cherry

# 4. -n1 -I {} bash -c を使用(簡単な条件分岐を含む)
@ blog_tmp % echo "apple banana cherry" | xargs -n1 -I {} bash -c 'if [ "{}" = "banana" ]; then echo "{} is yellow"; else echo "{} is not yellow"; fi'
apple is not yellow
banana is yellow
cherry is not yellow

コマンド

doneフォルダへ移動するコマンド

aws s3api list-objects-v2 --bucket cm-kasama-cli-test --prefix test-1/ --query 'Contents[?!contains(Key, `/done/`) && !contains(Key, `header`) && !ends_with(Key, `/`)].Key' --output text --profile <my-role> | \
xargs -n1 -I {} bash -c 'aws s3 mv s3://cm-kasama-cli-test/{} s3://cm-kasama-cli-test/$(dirname {})/done/$(basename {}) --profile <my-role>'

このコマンドの気になるポイントを分解してみていきます。

  • クエリによるフィルタリング:
    --query 'Contents[?!contains(Key, `/done/`) && !contains(Key, `header`) && !ends_with(Key, `/`)].Key'
    
    • Contents[?...]: Contents 配列の中で? の後に続く条件を満たす要素を抽出
    • !contains(Key, /done/): Keyに「/done/」を含まないもの
    • !contains(Key, header): Keyに「header」を含まないもの
    • !ends_with(Key, /): Keyが「/」で終わらないもの(ディレクトリを除外)
    • .Key: フィルタリングされたオブジェクトのKeyのみを選択

https://dev.classmethod.jp/articles/aws-cli-query-jmspath-tostring-grep/

queryオプションとoutputオプションをつけないで実行してみたら構造がわかりやすいと思います。

@ blog_tmp % aws s3api list-objects-v2 --bucket cm-kasama-cli-test --prefix test-1/  --profile <my-role>
{
    "Contents": [
        {
            "Key": "test-1/test-a/done/",
            "LastModified": "2024-08-28T13:24:36+00:00",
            "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
            "Size": 0,
            "StorageClass": "STANDARD"
        },
        {
            "Key": "test-1/test-a/done/test_a.txt",
            "LastModified": "2024-08-28T13:24:45+00:00",
            "ETag": "\"bf072e9119077b4e76437a93986787ef\"",
            "Size": 2,
            "StorageClass": "STANDARD"
        },
        {
            "Key": "test-1/test-a/test_a_header.txt",
            "LastModified": "2024-08-28T13:24:32+00:00",
            "ETag": "\"d6d147ba9012f08f6d4c62958a59136b\"",
            "Size": 9,
            "StorageClass": "STANDARD"
        },
        {
            "Key": "test-1/test-b/done/",
            "LastModified": "2024-08-28T13:24:37+00:00",
            "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
            "Size": 0,
            "StorageClass": "STANDARD"

xargs前までで実行した出力になります。

@ blog_tmp % aws s3api list-objects-v2 --bucket cm-kasama-cli-test --prefix test-1/ --query 'Contents[?!contains(Key, `/done/`) && !contains(Key, `header`) && !ends_with(Key, `/`)].Key' --output text --profile <my-role>
test-1/test-a/test_a.txt        test-1/test-b/test_b.txt        test-1/test-c/test_c.txt
  • xargsによる各オブジェクトの処理:
    xargs -n1 -I {} bash -c '...'
    
    • -n1: 1行ずつ処理するオプション
    • -I {}: 入力を{}プレースホルダー(※)で置換

※ プレースホルダー:後で実際の値や式で置き換えられる一時的な代替シンボルや文字列

  • 各オブジェクトの移動処理:
    'aws s3 mv s3://cm-kasama-cli-test/{} s3://cm-kasama-cli-test/$(dirname {})/done/$(basename {}) --profile <my-role>'
    
    • s3://cm-kasama-cli-test/{}: 元のオブジェクトのパスがそのまま入る
    • s3://cm-kasama-cli-test/$(dirname {})/done/$(basename {}): 移動先のパス
      • $(dirname {}): オブジェクトのディレクトリパスを取得
      • $(basename {}): オブジェクトのファイル名を取得

https://qiita.com/frozencatpisces/items/278a9abf4343fb690ad6

最後に出力結果となります。

@ tmp % aws s3api list-objects-v2 --bucket cm-kasama-cli-test --prefix test-1/ --query 'Contents[?!contains(Key, `/done/`) && !contains(Key, `header`) && !ends_with(Key, `/`)].Key' --output text --profile <my-role> | \
xargs -n1 -I {} bash -c 'aws s3 mv s3://cm-kasama-cli-test/{} s3://cm-kasama-cli-test/$(dirname {})/done/$(basename {}) --profile <my-role>'
move: s3://cm-kasama-cli-test/test-1/test-a/test_a.txt to s3://cm-kasama-cli-test/test-1/test-a/done/test_a.txt
move: s3://cm-kasama-cli-test/test-1/test-b/test_b.txt to s3://cm-kasama-cli-test/test-1/test-b/done/test_b.txt
move: s3://cm-kasama-cli-test/test-1/test-c/test_c.txt to s3://cm-kasama-cli-test/test-1/test-c/done/test_c.txt

doneフォルダから1階層前へ移動するコマンド

今度は逆に「done」ディレクトリ内のファイルを元の場所に戻すコマンドを実行したいと思います。

aws s3api list-objects-v2 --bucket cm-kasama-cli-test --prefix test-1/ --query 'Contents[?contains(Key, `/done/`) && !ends_with(Key, `/`)].Key' --output text --profile <my-role> | \
xargs -n1 -I {} bash -c 'aws s3 mv s3://cm-kasama-cli-test/{} s3://cm-kasama-cli-test/$(dirname $(dirname {}))/$(basename {}) --profile <my-role>'

このコマンドも同様の構造を持っていますが、いくつかの違いがあります。queryオプションで、 /done/ を含むファイルを抽出しています。

@ blog_tmp % aws s3api list-objects-v2 --bucket cm-kasama-cli-test --prefix test-1/ --query 'Contents[?contains(Key, `/done/`) && !ends_with(Key, `/`)].Key' --output text --profile <my-role>
test-1/test-a/done/test_a.txt   test-1/test-b/done/test_b.txt   test-1/test-c/done/test_c.txt

aws mvコマンドでは、$(dirname $(dirname {})) を使用して、「done」ディレクトリの1つ上の階層を指定しています。$(dirname $(dirname {})) は内側から外側へと順に処理が行われます。dirname は与えられたパスの最後の部分(ファイル名やディレクトリ名)を除いた親ディレクトリのパスを返します。例えば、test-1/test-a/done/test_a.txt に対して

  1. 内側の dirname 操作:

    $(dirname test-1/test-a/done/test_a.txt)
    

    結果は、最後の test_a.txt が除かれてtest-1/test-a/doneになります。

  2. 外側の dirname 操作:

    $(dirname test-1/test-a/done)
    

    結果は、最後の done が除かれてtest-1/test-aになります。

つまり、$(dirname $(dirname test-1/test-a/done/test_a.txt)) は以下の順で処理され、最終的な結果はtest-1/test-aになります。

  1. test-1/test-a/done/test_a.txttest-1/test-a/done
  2. test-1/test-a/donetest-1/test-a

最後に実行結果になります。

@ blog_tmp % aws s3api list-objects-v2 --bucket cm-kasama-cli-test --prefix test-1/ --query 'Contents[?contains(Key, `/done/`) && !ends_with(Key, `/`)].Key' --output text --profile <my-role> | \
xargs -n1 -I {} bash -c 'aws s3 mv s3://cm-kasama-cli-test/{} s3://cm-kasama-cli-test/$(dirname $(dirname {}))/$(basename {}) --profile <my-role>'
move: s3://cm-kasama-cli-test/test-1/test-a/done/test_a.txt to s3://cm-kasama-cli-test/test-1/test-a/test_a.txt
move: s3://cm-kasama-cli-test/test-1/test-b/done/test_b.txt to s3://cm-kasama-cli-test/test-1/test-b/test_b.txt
move: s3://cm-kasama-cli-test/test-1/test-c/done/test_c.txt to s3://cm-kasama-cli-test/test-1/test-c/test_c.txt

doneフォルダ作成コマンド

検証する上でdoneフォルダは、以下のコマンドを叩いて作っていましたが冗長なので、xargsを使用する形に修正します。

aws s3api put-object --bucket cm-kasama-cli-test --key "test-1/test-a/done/" --profile <my-role>
aws s3api put-object --bucket cm-kasama-cli-test --key "test-1/test-b/done/" --profile <my-role>
aws s3api put-object --bucket cm-kasama-cli-test --key "test-1/test-c/done/" --profile <my-role>

ファイルパスを記載したtxtファイルを作成しておいてそのcatコマンドの結果をaws s3apiコマンドのkeyとして使用しています。

put_commands.txt
test-1/test-a/done/
test-1/test-b/done/
test-1/test-c/done/
@ blog_command_test % cat put_commands.txt | xargs -n1 -I {} aws s3api put-object --bucket cm-kasama-cli-test --key {} --profile <my-role> 
{
    "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
    "ServerSideEncryption": "AES256"
}
{
    "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
    "ServerSideEncryption": "AES256"
}
{
    "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
    "ServerSideEncryption": "AES256"
}

最後に

業務で実際に使っている人がいて、見よう見まねでこれまで使用していたので、今回調べてみました。コマンドの意味についてある程度知ることができたので、今後はこのブログを思い出して活用していきたいと思います。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.